﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure;
using Microsoft.Extensions.Internal;

namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Filters;

internal abstract class SaveTempDataPropertyFilterBase : ISaveTempDataCallback
{
    protected readonly ITempDataDictionaryFactory _tempDataFactory;

    public SaveTempDataPropertyFilterBase(ITempDataDictionaryFactory tempDataFactory)
    {
        _tempDataFactory = tempDataFactory;
    }

    /// <summary>
    /// Describes the temp data properties which exist on <see cref="Subject"/>
    /// </summary>
    public IReadOnlyList<LifecycleProperty> Properties { get; set; }

    /// <summary>
    /// The <see cref="object"/> which has the temp data properties.
    /// </summary>
    public object Subject { get; set; }

    /// <summary>
    /// Tracks the values which originally existed in temp data.
    /// </summary>
    public IDictionary<PropertyInfo, object> OriginalValues { get; } = new Dictionary<PropertyInfo, object>();

    /// <summary>
    /// Puts the modified values of <see cref="Subject"/> into <paramref name="tempData"/>.
    /// </summary>
    /// <param name="tempData">The <see cref="ITempDataDictionary"/> to be updated.</param>
    public void OnTempDataSaving(ITempDataDictionary tempData)
    {
        if (Subject == null)
        {
            return;
        }

        for (var i = 0; i < Properties.Count; i++)
        {
            var property = Properties[i];
            OriginalValues.TryGetValue(property.PropertyInfo, out var originalValue);

            var newValue = property.GetValue(Subject);
            if (newValue != null && !newValue.Equals(originalValue))
            {
                tempData[property.Key] = newValue;
            }
        }
    }

    /// <summary>
    /// Sets the values of the properties of <see cref="Subject"/> from <paramref name="tempData"/>.
    /// </summary>
    /// <param name="tempData">The <see cref="ITempDataDictionary"/>.</param>
    protected void SetPropertyValues(ITempDataDictionary tempData)
    {
        if (Properties == null)
        {
            return;
        }

        Debug.Assert(Subject != null, "Subject must be set before this method is invoked.");

        for (var i = 0; i < Properties.Count; i++)
        {
            var property = Properties[i];
            var value = tempData[property.Key];

            OriginalValues[property.PropertyInfo] = value;
            property.SetValue(Subject, value);
        }
    }

    public static IReadOnlyList<LifecycleProperty> GetTempDataProperties(
        TempDataSerializer tempDataSerializer,
        Type type)
    {
        List<LifecycleProperty> results = null;
        var errorMessages = new List<string>();

        var propertyHelpers = PropertyHelper.GetVisibleProperties(type: type);
        for (var i = 0; i < propertyHelpers.Length; i++)
        {
            var propertyHelper = propertyHelpers[i];
            var property = propertyHelper.Property;
            var tempDataAttribute = property.GetCustomAttribute<TempDataAttribute>();
            if (tempDataAttribute != null && ValidateProperty(tempDataSerializer, errorMessages, propertyHelper.Property))
            {
                if (results == null)
                {
                    results = new List<LifecycleProperty>();
                }

                var key = tempDataAttribute.Key;
                if (string.IsNullOrEmpty(key))
                {
                    key = property.Name;
                }

                results.Add(new LifecycleProperty(property, key));
            }
        }

        if (errorMessages.Count > 0)
        {
            throw new InvalidOperationException(string.Join(Environment.NewLine, errorMessages));
        }

        return results;
    }

    private static bool ValidateProperty(TempDataSerializer tempDataSerializer, List<string> errorMessages, PropertyInfo property)
    {
        if (!(property.SetMethod != null &&
            property.SetMethod.IsPublic &&
            property.GetMethod != null &&
            property.GetMethod.IsPublic))
        {
            errorMessages.Add(
                Resources.FormatTempDataProperties_PublicGetterSetter(property.DeclaringType.FullName, property.Name, nameof(TempDataAttribute)));

            return false;
        }

        if (!tempDataSerializer.CanSerializeType(property.PropertyType))
        {
            var errorMessage = Resources.FormatTempDataProperties_InvalidType(
                tempDataSerializer.GetType().FullName,
                TypeNameHelper.GetTypeDisplayName(property.DeclaringType),
                property.Name,
                TypeNameHelper.GetTypeDisplayName(property.PropertyType));

            errorMessages.Add(errorMessage);

            return false;
        }

        return true;
    }
}
